/**
 * 
 */
package cz.cuni.mff.abacs.burglar.visual.multithreading;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import cz.cuni.mff.abacs.burglar.logics.DataMap;
import cz.cuni.mff.abacs.burglar.logics.GameMap;
import cz.cuni.mff.abacs.burglar.logics.objects.agents.Burglar;
import cz.cuni.mff.abacs.burglar.logics.objects.agents.Guard;
import cz.cuni.mff.abacs.burglar.logics.objects.positions.Door;
import cz.cuni.mff.abacs.burglar.logics.planning.PlannerHandler;
import cz.cuni.mff.abacs.burglar.logics.planning.PlannerHandlerFactory;
import cz.cuni.mff.abacs.burglar.logics.planning.instructions.Instruction;
import cz.cuni.mff.abacs.tools.CombinationGenerator;


/**
 * 
 * 
 * @author abacs
 *
 */
public class PlanningThread extends Thread {
	
	// constants:
	
	/** 
	 * 
	 *  
	 *  */
	public enum Type {
		PLANNING,
		PLANNING_BURGLAR,
		SELECTING_TRAP_ROOMS,
	}
	
	
	// -------------------------------------------------------------------------
	
	
	/** Address to return the results */
	private final PlanningListener _listener;
	
	/** Object to execute the planning. */
	private final PlannerHandler _planner = 
		PlannerHandlerFactory.createPlannerHandler();
	
	/** Map to execute planning on */
	private final GameMap _map;
	
	/**  */
	private final Type _type;
	
	
	// -------------------------------------------------------------------------
	// constructors:
	
	
	/**
	 * 
	 * 
	 * @param map
	 * @param type
	 * @param listener
	 * @param planner
	 */
	public PlanningThread(
			GameMap map,
			Type type,
			PlanningListener listener
	) {
		super();
		this._map = map;
		this._type = type;
		this._listener = listener;
	}
	
	
	// -------------------------------------------------------------------------
	
	
	/* (non-Javadoc)
	 * @see java.lang.Runnable#run()
	 */
	@Override
	public void run() {
		
		long startTime = System.currentTimeMillis();
		
		switch(this._type){
		case PLANNING:
			// the order of the following two instructions is important!
			// planBurglar does removes the planning flag
			this.planGuards();
			this.planBurglar();
			break;
		case PLANNING_BURGLAR:
			this.planBurglar();
			break;
		case SELECTING_TRAP_ROOMS:
			this.selectTrapRooms();
			break;
		}
		
		// print out the planning duration:
		long endTime = System.currentTimeMillis();
		long duration = endTime - startTime;
		
		System.out.println(
				"- Planning time: " + duration + " ms. (" + this._type + ')'
		);
		
			
	}
	
	
	// -------------------------------------------------------------------------
	
	
	/**
	 * 
	 */
	private void plan() {
		List<Instruction> instructions = 
			this._planner.solveProblem(this._map);
		
		this._listener.planningFinished(instructions, this);
	}
	
	
	/**
	 * 
	 */
	private void planBurglar() {
		
		List<Integer> emptyList = new LinkedList<Integer>();
		List<Integer> avoidedTrapRooms = new ArrayList<Integer>();
		
		// instructions that the burglar would normally follow:
		List<Instruction> freeInstructions = 
			this._planner.solveBurglarProblem(
					this._map,
					emptyList,
					emptyList
			);
		
		// for each trap in the map control whether the avoiding plan matches 
		// the burglar's normal plan
		for(Integer trapRoomId : this._map.getTrapRoomIds()){
			
			List<Integer> trapRooms = new LinkedList<Integer>();
			trapRooms.add(trapRoomId);
			
			List<Instruction> avoidInstructions = 
				this._planner.solveBurglarProblem(
						this._map,
						emptyList,
						trapRooms
				);
			
			if(this._map.instructionListsMatch(freeInstructions, avoidInstructions)){
				System.out.println("- trap room " + trapRoomId + " avoided");
				avoidedTrapRooms.add(trapRoomId);
			}
			
		}
		
		this._listener.planningFinished(freeInstructions, avoidedTrapRooms, this);
	}
	
	
	/**
	 * 
	 */
	private void planGuards() {

		List<Instruction> result = new LinkedList<Instruction>();
		List<Integer> emptyList = new LinkedList<Integer>();
		
		for(Guard guard : this._map.getGuards()){
			
			// instructions that the burglar would normally follow:
			List<Instruction> instructions = 
				this._planner.solveGuardProblem(
						guard,
						this._map,
						emptyList,
						emptyList
				);
			result.addAll(instructions);
		}
		
		this._listener.planningFinished(result, this);
	}
	
	
	/**
	 * 
	 */
	private void selectTrapRooms() {
		
		// 
		// plan a burglar path to get the needed rooms:
		// 
		
		List<Integer> emptyList = new LinkedList<Integer>();
		
		// instructions that the burglar would normally follow:
		List<Instruction> originalPlan = 
			this._planner.solveBurglarProblem(this._map, emptyList, emptyList);
		
		List<Integer> originalRooms = 
				getPlannedRoomIds(originalPlan, (DataMap)this._map);
		// remove current room:
		Burglar burglar = this._map.getBurglar();
		if(originalRooms.contains(burglar.getRoomId()))
			originalRooms.remove(originalRooms.indexOf(burglar.getRoomId()));
		if(originalRooms.contains(burglar.getAimId()))
			originalRooms.remove(originalRooms.indexOf(burglar.getAimId()));
		
		// remove the rooms that can't be walked around:
		List<Integer> trapRooms = new ArrayList<Integer>(1);
		List<Integer> roomsToControl = new ArrayList<Integer>(originalRooms.size());
		for(Integer roomId : originalRooms)
			roomsToControl.add(roomId);
		
		for(Integer roomId : roomsToControl){
			// test the trap room configuration:
			
			trapRooms.add(roomId);
			List<Instruction> restrictedPlan = 
				this._planner.solveBurglarProblem(this._map, emptyList, trapRooms);
			
			System.out.println("is room required? id: "+roomId+" "+restrictedPlan.isEmpty());
			
			// exit if plan is not valid, current room is needed:
			if(restrictedPlan.isEmpty()){
				originalRooms.remove(originalRooms.indexOf(roomId));
			}
			
			trapRooms.clear();
		}
		
		// TODO
		debugPrint(
				"original path rooms (" + originalRooms.size() + "): ",
				originalRooms, ""
		);
		
		//
		// insert trap rooms:
		//
		
		int trapRoomCount = this._map.getRequiredTrapRoomCount();
		if(trapRoomCount > originalRooms.size())
			trapRoomCount = originalRooms.size();
		
		// try the maximal number of trap rooms the user wants, if not possible
		// try less:
		trapRoomCount++;
		while(trapRooms.isEmpty() && --trapRoomCount > 0){
			
			PseudoRandomCombinationGenerator gener = 
					new PseudoRandomCombinationGenerator(
							originalRooms.size(),
							trapRoomCount
					);
			
			// try all combinations of trap rooms for the current trap room count
			while(gener.hasMore()){
				// generate a trap room configuration:
				int[] combination = gener.getNext();
				
				selectSublist(originalRooms, combination, trapRooms);
				
				// test the trap room configuration:
				List<Instruction> restrictedPlan = 
					this._planner.solveBurglarProblem(this._map, emptyList, trapRooms);
				
				// TODO
				debugPrint(
						"trap rooms (" + trapRoomCount + ") tried: ",
						trapRooms,
						"does it work: " + !restrictedPlan.isEmpty()
				);
				
				// exit if plan is valid:
				if(restrictedPlan.isEmpty())
					trapRooms.clear();
				else
					break;
			}
		}
		
		//
		// insert traps to trap rooms
		//
		
		// TODO
		debugPrint("trap rooms that worked out ("+trapRoomCount+"): ", trapRooms, "");
		
		this._listener.selectingTrapRoomsFinished(trapRooms, this);
		
		return;
	}
	
	
	/**
	 * Returns the rooms that the agent plans to visit.
	 * 
	 * @param instructions
	 * @param referenceMap
	 * @return
	 */
	protected static List<Integer> getPlannedRoomIds(
			List<Instruction> instructions, 
			DataMap referenceMap
	) {
		Set<Integer> rooms = new HashSet<Integer>();
		
		for(Instruction instr : instructions){
			if(instr._code == Instruction.code.ENTER_DOOR){
				Door subject = (Door)referenceMap.getPosition(instr._subjectId);
				int[] doorRooms = subject.getRoomIds();
				rooms.add(doorRooms[0]);
				rooms.add(doorRooms[1]);
			}
		}
		
		List<Integer> result = new ArrayList<Integer>(rooms.size());
		for(Integer roomId : rooms)
			result.add(roomId);
		
		return result;
	}
	
	
	private static void selectSublist(
			List<Integer> source,
			int[] indexes,
			List<Integer> destination
	) {
		destination.clear();
		for(int i = 0; i < indexes.length; i++){
			destination.add(source.get(indexes[i]));
		}
	}
	
	
	/**
	 * 
	 * @param leadingText
	 * @param ids
	 * @param endingText
	 */
	private static void debugPrint(
			String leadingText,
			List<Integer> ids,
			String endingText
	) {
		System.out.print(leadingText);
		for(Integer id : ids)System.out.print(id + ", ");
		System.out.println(endingText);
	}
	
	
	// -------------------------------------------------------------------------
	
	
	class PseudoRandomCombinationGenerator {
		
		
		/**
		 * 
		 */
		private final int BUFFER_SIZE = 500;
		
		
		// ---------------------------------------------------------------------
		
		
		/**
		 * 
		 */
		private final CombinationGenerator _gener;
		
		
		/**
		 * 
		 */
		private final List<int[]> _buffer = new ArrayList<int[]>(BUFFER_SIZE);
		
		
		// ---------------------------------------------------------------------
		// constructors:
		
		
		/**
		 * 
		 * @param n
		 * @param r
		 */
		public PseudoRandomCombinationGenerator(int n, int r) {
			this._gener = new CombinationGenerator(n, r);
			this.fillBuffer();
		}
		
		
		// ---------------------------------------------------------------------
		
		
		/**
		 * 
		 * @return
		 */
		public boolean hasMore() {
			return this._buffer.isEmpty() == false || this._gener.hasMore();
		}
		
		
		/**
		 * 
		 * @return
		 */
		public int[] getNext() {
			if(this._buffer.isEmpty()){
				if(this._gener.hasMore()){
					this.fillBuffer();
					return this.getNext();
				}
				return null;
			}
			return this._buffer.remove(0);
		}
		
		
		// ---------------------------------------------------------------------
		
		
		/**
		 * 
		 */
		private void fillBuffer() {
			while(this._buffer.size() < BUFFER_SIZE && this._gener.hasMore()){
				this._buffer.add(this._gener.getNext().clone());
			}
			
			Collections.shuffle(this._buffer);
		}
		
		
	}
	
	
}
